Skip to content

fix: real-time streak calculation#33

Open
tomasmach wants to merge 3 commits intodevfrom
fix/this-week
Open

fix: real-time streak calculation#33
tomasmach wants to merge 3 commits intodevfrom
fix/this-week

Conversation

@tomasmach
Copy link
Copy Markdown
Owner

@tomasmach tomasmach commented Jan 28, 2026

Problem

Dashboard showed stale streak values when users stopped writing. The user.current_streak was only updated when creating new entries.

Solution

Calculate current streak in real-time from actual entries:

  • Streak = 0 if last entry was 2+ days ago
  • Streak continues if last entry was today or yesterday
  • longest_streak still comes from user model (historical)

Changes

  • Added calculate_current_streak() method to DashboardView
  • 9 new tests covering timezone handling, gaps, empty entries

Testing

pytest apps/api/tests/test_dashboard_streak.py -v
# 9 passed

Summary by CodeRabbit

  • New Features

    • Dashboard now refreshes automatically once after creating or saving an entry so streaks update promptly.
  • Bug Fixes

    • Streaks computed in real time from entries (not stale cached values).
    • Current streak may include yesterday if appropriate.
    • Streaks reset correctly after gaps and respect user timezones.
    • Empty entries no longer count toward streak totals.
  • Tests

    • Added comprehensive integration tests covering streak edge cases and timezones.

✏️ Tip: You can customize this high-level summary in your review settings.

Previously, user.current_streak was only updated when creating new entries.
This caused the dashboard to show stale streak values when users stopped
writing for 2+ days.

Changes:
- Add calculate_current_streak() method to DashboardView
- Calculate streak from actual entries in real-time
- Streak shows 0 if last entry was 2+ days ago
- Streak continues if last entry was today or yesterday
- Preserve longest_streak from user model (historical record)

Adds 9 comprehensive tests for the new streak calculation logic.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

Adds real-time current_streak calculation (optionally anchored to yesterday) used by DashboardView and a new integration test module validating streak behavior across timezones, gaps, empty entries, and combinations of historical/current-day entries.

Changes

Cohort / File(s) Summary
New tests
apps/api/tests/test_dashboard_streak.py
New integration test module TestDashboardStreakRealTime with 9 tests covering zero-after-gap, writing today/yesterday, no entries, preserving longest_streak, recent-activity-after-gap, timezone-aware cases (Tokyo, LA), and ignoring empty entries.
Dashboard view
apps/api/views.py
DashboardView.get() now calls recalculate_user_streak(user, allow_yesterday=True) and uses the resulting current_streak in the stats payload instead of user.current_streak.
Streak utility
apps/journal/utils.py
recalculate_user_streak(user, allow_yesterday=False) signature added; logic updated to optionally anchor streak calculations to yesterday when allow_yesterday=True, adjusting the backward-date scan.
Frontend consumers
frontend/src/pages/EntryEditorPage.tsx, frontend/src/pages/TodayEntryPage.tsx, frontend/src/hooks/useDashboard.ts
useDashboard() now exposes refresh; pages call refresh (as refreshDashboard) once after the first successful save to update dashboard/streak UI.

Sequence Diagram

sequenceDiagram
    participant Client
    participant DashboardView as DashboardView
    participant StreakUtil as recalculate_user_streak
    participant EntryModel as EntryModel
    participant Database as Database

    Client->>DashboardView: GET /api/v1/dashboard/
    DashboardView->>StreakUtil: recalculate_user_streak(user, allow_yesterday=True)
    StreakUtil->>EntryModel: Query entries for user (word_count>0)
    EntryModel->>Database: SELECT entries ORDER BY created
    Database-->>EntryModel: Return entries
    EntryModel-->>StreakUtil: Entry list (dates filtered)
    StreakUtil->>StreakUtil: Determine anchor (today or yesterday) and compute streak
    StreakUtil-->>DashboardView: current_streak
    DashboardView->>Client: Return dashboard stats with current_streak
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Bug fixes #10 — touches streak and empty-entry handling, overlaps with recalculate_user_streak behavior and tests.
  • Feat/dashboard changes #17 — modifies DashboardView.get and dashboard response construction, overlaps with the new real-time current_streak usage.

Poem

🐰 I hopped through dates with delight,
Counting lines by day and night.
Timezones bent, gaps I mend,
Today or yesterday — I friend.
Streaks refreshed, my tally bright.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title 'fix: real-time streak calculation' accurately reflects the main change—computing current streak values in real-time from actual entries instead of using stale user.current_streak.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/api/tests/test_dashboard_streak.py`:
- Around line 156-158: Update the misleading inline comment above the
EntryFactory call in test_dashboard_streak.py: replace the incorrect timezone
conversion ("2025-01-13 17:00 UTC = 2025-01-14 09:00 LA time") with the correct
conversion reflecting LA = UTC-8 (e.g., "2025-01-13 17:00 UTC = 2025-01-13 09:00
LA time") so the comment correctly describes why EntryFactory(user=user,
created_at=timezone.now() - timedelta(days=1, hours=8)) yields an entry on Jan
13 in LA time.

In `@apps/api/views.py`:
- Line 226: Remove the redundant local import "from apps.journal.models import
Entry" in apps/api/views.py (it's already imported at module level as Entry);
locate the inline import inside the function or block and delete that line so
the code uses the existing top-level Entry import and avoids duplicate imports.
🧹 Nitpick comments (3)
apps/api/views.py (2)

30-30: Consider grouping Django imports together.

The TruncDate import from django.db.models.functions should be grouped with other Django imports (lines 13-21) for better organization.

♻️ Suggested import organization

Move line 30 up to group with other Django imports:

 from django.db.models import Sum, Count, Q
+from django.db.models.functions import TruncDate
 from django.db import transaction
 from django.core.cache import cache
 from django.utils import timezone
 
 from apps.journal.models import Entry, FeaturedEntry
 from apps.journal.utils import (
     get_random_quote,
     get_user_local_date,
     get_today_date_range,
     parse_tags,
 )
-from django.db.models.functions import TruncDate
 from apps.api.serializers import (

233-245: Consider optimizing to avoid potential double query evaluation.

The queryset is evaluated twice: once for exists() (line 241) and once when iterating to build the dates list (line 245). You could simplify this by fetching the list directly and checking its length.

♻️ Suggested optimization
         # Get all days with entries (with word_count > 0)
-        entry_dates = (
+        dates = list(
             Entry.objects.filter(user=user, word_count__gt=0)
             .annotate(day=TruncDate('created_at', tzinfo=user_tz))
-            .values('day')
-            .distinct()
+            .values_list('day', flat=True)
             .order_by('day')
+            .distinct()
         )
         
-        if not entry_dates.exists():
+        if not dates:
             return 0
-        
-        # Get unique dates as a list
-        dates = sorted(set(item['day'] for item in entry_dates))
apps/api/tests/test_dashboard_streak.py (1)

18-21: Add @pytest.mark.streak domain marker per coding guidelines.

The coding guidelines specify using domain markers for tests. Since these tests cover streak functionality, add the @pytest.mark.streak marker.

♻️ Suggested fix
 `@pytest.mark.integration`
+@pytest.mark.streak
 `@pytest.mark.django_db`
 class TestDashboardStreakRealTime:
     """Tests for real-time streak calculation in dashboard."""

As per coding guidelines: "Use pytest with markers (@pytest.mark.unit, @pytest.mark.integration, @pytest.mark.slow) and domain markers (@pytest.mark.encryption, @pytest.mark.streak, @pytest.mark.statistics)"

Comment on lines +156 to +158
# Entry from "yesterday" in LA timezone (which is 2 days ago UTC)
# 2025-01-13 17:00 UTC = 2025-01-14 09:00 LA time (yesterday for LA user)
EntryFactory(user=user, created_at=timezone.now() - timedelta(days=1, hours=8))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix incorrect comment - timezone math is wrong.

The comment states "2025-01-13 17:00 UTC = 2025-01-14 09:00 LA time" but LA is UTC-8, so the correct conversion is:

  • 2025-01-13 17:00 UTC = 2025-01-13 09:00 LA time

The test assertion is still correct (entry is on Jan 13 LA time, which is "yesterday" when LA's current date is Jan 14), but the comment is misleading.

📝 Suggested comment fix
         # Entry from "yesterday" in LA timezone (which is 2 days ago UTC)
-        # 2025-01-13 17:00 UTC = 2025-01-14 09:00 LA time (yesterday for LA user)
+        # 2025-01-13 17:00 UTC = 2025-01-13 09:00 LA time (yesterday for LA user since LA is on Jan 14)
         EntryFactory(user=user, created_at=timezone.now() - timedelta(days=1, hours=8))
🤖 Prompt for AI Agents
In `@apps/api/tests/test_dashboard_streak.py` around lines 156 - 158, Update the
misleading inline comment above the EntryFactory call in
test_dashboard_streak.py: replace the incorrect timezone conversion ("2025-01-13
17:00 UTC = 2025-01-14 09:00 LA time") with the correct conversion reflecting LA
= UTC-8 (e.g., "2025-01-13 17:00 UTC = 2025-01-13 09:00 LA time") so the comment
correctly describes why EntryFactory(user=user, created_at=timezone.now() -
timedelta(days=1, hours=8)) yields an entry on Jan 13 in LA time.

@tomasmach
Copy link
Copy Markdown
Owner Author

Code review

Found 2 issues:

  1. Missing @pytest.mark.streak domain marker on test class (CLAUDE.md says "Domain markers: @pytest.mark.encryption, @pytest.mark.streak, @pytest.mark.statistics")

All 9 tests in this file exercise streak logic, but the class only declares @pytest.mark.integration and @pytest.mark.django_db. Running uv run pytest -m streak will miss these tests entirely.

@pytest.mark.django_db
class TestDashboardStreakRealTime:
"""Tests for real-time streak calculation in dashboard."""

  1. Commit message uses forbidden scoped prefix (~/.claude/CLAUDE.md says "Never add scopes - do NOT use feat(scope): format, only use simple feat: format")

The commit message is fix(dashboard): calculate current streak in real-time. The (dashboard) scope matches the explicitly listed incorrect pattern fix(auth): resolve bug. Should be fix: calculate current streak in real-time.

QuietPage/apps/api/views.py

Lines 207 to 268 in d4fc7e1

def calculate_current_streak(self, user):
"""
Calculate current writing streak in real-time based on actual entries.
Unlike user.current_streak which is only updated when creating entries,
this method always returns the accurate current streak by checking
if the user wrote yesterday (streak continues) or not (streak is 0).
Logic:
- Streak continues if last entry was today or yesterday
- Streak breaks if last entry was 2+ days ago
- Returns 0 if no entries or streak is broken
Args:
user: User object with timezone field
Returns:
int: Current consecutive days writing streak (0 if broken)
"""
from apps.journal.models import Entry
user_tz = ZoneInfo(str(user.timezone))
today = get_user_local_date(timezone.now(), user.timezone)
yesterday = today - timedelta(days=1)
# Get all days with entries (with word_count > 0)
entry_dates = (
Entry.objects.filter(user=user, word_count__gt=0)
.annotate(day=TruncDate('created_at', tzinfo=user_tz))
.values('day')
.distinct()
.order_by('day')
)
if not entry_dates.exists():
return 0
# Get unique dates as a list
dates = sorted(set(item['day'] for item in entry_dates))
# Check if streak is still active (last entry is today or yesterday)
last_entry_date = dates[-1]
if last_entry_date not in (today, yesterday):
# Streak is broken - user hasn't written yesterday or today
return 0
# Calculate streak by working backwards from last entry date
current_streak = 0
check_date = last_entry_date
for entry_date in reversed(dates):
if entry_date == check_date:
current_streak += 1
check_date -= timedelta(days=1)
else:
break
return current_streak
def serialize_featured_entry(self, entry, user_date):
"""Serialize featured entry for API response."""
if not entry:

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@tomasmach tomasmach changed the title fix(dashboard): real-time streak calculation fix: real-time streak calculation Jan 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant